#[macro_use]
extern crate clap;
#[macro_use]
extern crate serde;
#[macro_use]
extern crate log;

use chrono::Local;
use clap::{App, Arg};
use colored::*;
use log::LevelFilter;
use reqwest::Client;
use serde_json::json;
use std::error::Error;
use std::io::Write;
use std::time::Duration;

// const define
const CONNECT_TIMEOUT_SECS: u64 = 2;
const READ_TIMEOUT_SECS: u64 = 2;
const MAX_TRY: u8 = 3;
// end const define

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TWResult {
    input: String,
    blng_sents_part: Option<BlngSentsPart>, // HACK: CamalCase
    phrs: Option<Phrs>,
    rel_word: Option<RelWord>,
    simple: Option<Simple>,
    ec: Option<Ec>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Simple {
    word: Vec<Phone>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Phone {
    us_phone: Option<String>,
    uk_phone: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BlngSentsPart {
    #[serde(alias = "sentence-pair")]
    sentence_pair: Vec<Sen>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Sen {
    sentence: String,
    #[serde(alias = "sentence-translation")]
    sentence_translation: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Phrs {
    phrs: Vec<Phr>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Phr {
    phr: PhrOne,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct PhrOne {
    headword: HeadWord,
    trs: Vec<Tr>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct HeadWord {
    l: L,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct L {
    i: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Tr {
    tr: HeadWord,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct RelWord {
    rels: Vec<Rel>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Rel {
    rel: RelOne,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct RelOne {
    pos: String,
    words: Vec<Word>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Word {
    word: String,
    tran: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Ec {
    word: Vec<EcWord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "word")]
struct EcWord {
    us_phone: Option<String>,
    uk_phone: Option<String>,
    trs: Vec<EcTr>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "tr")]
struct EcTr {
    tr: Vec<EcL>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "l")]
struct EcL {
    l: EcI,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "tr")]
struct EcI {
    i: Vec<String>,
}

impl Ec {
    fn text(&self) -> String {
        let mut result = String::new();
        self.word.iter().for_each(|x| {
            result.push_str(
                format!(
                    // ERROR: not show the to_translate word
                    // "美{}, 英{}\n\n",
                    // x.us_phone.as_deref().unwrap_or(""),
                    // x.uk_phone.as_deref().unwrap_or(""),
                    "{}",
                    String::from("[curl OK]\n\n"),
                )
                .as_str(),
            );
            x.trs.iter().for_each(|y| {
                result.push_str(format!("- {}\n", &y.tr[0].l.i[0]).as_str());
            })
        });
        result // the return value
    } // text
}

impl BlngSentsPart {
    fn text(&self) -> String {
        let mut result = String::new();
        self.sentence_pair.iter().for_each(|one| {
            result.push_str(
                format!["- {}\n {}\n", &one.sentence, &one.sentence_translation].as_str(),
            );
        });
        result
    }
}

impl RelWord {
    fn text(&self) -> String {
        let mut result = String::new();
        self.rels.iter().for_each(|x| {
            result.push_str(format!["- {}\n", &x.rel.pos].as_str());
            x.rel.words.iter().for_each(|y| {
                result.push_str(format!["\t{} {}\n", &y.word, &y.tran].as_str());
            });
        });
        result // the return value
    }
}

impl Phrs {
    fn text(&self) -> String {
        let mut result = String::new();
        self.phrs.iter().take(3).for_each(|one| {
            result.push_str(
                format!["- {} {}\n", &one.phr.headword.l.i, &one.phr.trs[0].tr.l.i].as_str(),
            );
        });
        result
    }
}

impl TWResult {
    pub fn text(&self) -> String {
        format!(
            r#"{}
[同根]
{}
[短语]
{}
[例句]
{}
        "#,
            self.ec
                .as_ref()
                .map_or(ColoredString::from(""), |ec| ec.text().red().bold()),
            self.rel_word
                .as_ref()
                .map_or(ColoredString::from(""), |rel| rel.text().blue().bold()),
            self.phrs
                .as_ref()
                .map_or(ColoredString::from(""), |phrs| phrs.text().purple().bold()),
            self.blng_sents_part
                .as_ref()
                .map_or(ColoredString::from(""), |blng| blng.text().cyan().bold()),
        )
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    init_logger();
    let matches = App::new(crate_name!())
        .version(crate_version!())
        .about(crate_description!())
        .arg(
            Arg::with_name("to_translate")
                .required(true)
                .help("word to translate"),
        )
        .get_matches();

    let to_translate = matches.value_of("to_translate").unwrap();

    let url = "https://dict.youdao.com/jsonapi?";// TODO: more source? like bing translate?
    let dicts = [[
        "blng_sents_part",
        "rel_word",
        "simple",
        "phrs",
        "meta",
        "ec",
    ]];

    let dicts = json!(
    {
        "count": 99,
        "dicts": dicts,
    }
    )
    .to_string();

    let client = Client::builder()
        .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
        .timeout(Duration::from_secs(READ_TIMEOUT_SECS))
        .user_agent("curl")
        .build()?;

    let mut times_left = MAX_TRY;

    loop {
        let response = client
            .get(url)
            .query(&[("q", to_translate)])
            .query(&[("dicts", &dicts)])
            .send()
            .await;
        match response {
            Ok(resp) => {
                let json_result: TWResult = resp.json().await?;
                println!("{}", &json_result.text());
                break;
            }
            Err(e) => {
                debug!("{:?}", e);

                if e.is_timeout() {
                    error!("Network timeout, retrying...");

                    if times_left > 0 {
                        times_left -= 1;
                        continue;
                    } else {
                        break;
                    }
                } else {
                    error!("{}", e);
                }
            }
        }
    }
    Ok(())
} // main

fn init_logger() {
    env_logger::Builder::new()
        .format(|buf, record| {
            writeln!(
                buf,
                "{} [{}] - {}",
                Local::now().format("%Y-%m-%d %H:%M:%S"),
                record.level(),
                record.args()
            )
        })
        .filter(None, LevelFilter::Info)
        .init();
}
